# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: jaewon

import datetime
import functools
import logging
import sys

from tornado import gen

from coin.base.datetime_util import to_datetime
from coin.base.timestamp import get_timestamp
from coin.exchange.base.order_gateway import OrderSide
from coin.exchange.bitflyer_v1.kr_rest.futures_product import (BitflyerCurrency,
                                                               BitflyerFuturesProduct)
from coin.exchange.bitmex.kr_rest.futures_product import BitmexFuturesProduct
from coin.exchange.okex_futures.kr_rest.futures_product import (OkexFuturesProduct)

from coin.strategy.mm.base.bookmap import BookMap
from coin.strategy.mm.base.bitflyer_strategy_base import (BitflyerMarginPositionBalancePrinter,
                                                          BitflyerStrategyBase,
                                                          run_live)
from coin.strategy.mm.base.dump.pass_unhedge import FeatureRow

from coin.strategy.mm.bitflyer_v1.pass_unhedge_pricer import Pricer
from coin.strategy.mm.bitflyer_v1.simple_mm_executor import (BitflyerSimpleMMExecutor)

from coin.exchange.base.strategy_logger import StrategyLogProvider
from coin.proto.coin_request_pb2 import StrategyRequestProto


class PassiveUnhedgeStrategy(BitflyerStrategyBase):
  def __init__(self, strategy_params, dry_run=True):
    trade_product = strategy_params['trade_product']
    super().__init__(products=[trade_product],
                     currencies=strategy_params['currencies'],
                     logger=logging.getLogger('PassiveUnhedgeStrategy'))

    self._strategy_params = strategy_params
    self._dry_run = dry_run

    self._trade_product = trade_product
    self._ref_product = strategy_params['ref_product']
    self._edge = strategy_params['edge_bp'] / 10000.
    self._close_edge = strategy_params['close_edge_bp'] / 10000.

    self._min_pos = float(strategy_params['executor']['min_pos'])
    self._max_pos = float(strategy_params['executor']['max_pos'])
    self._pos_bias = float(strategy_params['pos_bias'])

    self._bookmap = BookMap([self._trade_product, self._ref_product], logger=self._logger)
    self._pricer = Pricer(**strategy_params['pricing'])
    self._executor = None

    request = StrategyRequestProto(**self._strategy_params['logger'])
    self._strategy_logger = StrategyLogProvider(request)
    self._strategy_logger.write_request()

    pos_bias_map = {self._trade_product: (self._pos_bias, 0.)}
    self._printer = BitflyerMarginPositionBalancePrinter(BitflyerCurrency.FromStrCurrency('JPY'),
                                                         self._trade_product,
                                                         self._bookmap,
                                                         pos_bias_map=pos_bias_map,
                                                         strategy_logger=self._strategy_logger,
                                                         logger=self._logger)

    self._last_feature_dump_ts = 0

  @property
  def strategy_logger(self):
    return self._strategy_logger

  def prepare(self):
    assert self.feed_subsystem is not None

    # Subscribe feed.
    for product in (self._trade_product, self._ref_product):
      self.feed_subsystem.subscribe_book(product, functools.partial(self.on_book, product))

    # Start balance and position printing.
    self._printer.start_print(self._ioloop, 10)

  def on_og_reset(self):
    # Cancel all unknown working orders.
    self._ioloop.add_callback(self.periodic_cancel_unknown_working_orders)

    self._printer.register_order_gateway(self.order_gateway)

    # Intialize passive executor
    self._logger.info('Intializing passive executor...')
    self._executor = BitflyerSimpleMMExecutor(self._strategy_params['executor'],
                                              self.order_gateway,
                                              self._logger)
    # TODO(jaewon):  self.order_gateway.register_fill_callback(self._on_fill)

  @gen.coroutine
  def periodic_cancel_unknown_working_orders(self):
    while True:
      if self.order_gateway.is_ready():
        self.order_gateway.cancel_unknown_working_orders()
      yield gen.sleep(30)

  def on_book(self, product, book):
    self._bookmap.set_book(product, book)

    # Check book freshness and sanity
    if not self._bookmap.is_book_ready() or not self._bookmap.is_book_sane():
      return

    if product not in (self._ref_product, self._trade_product):
      return

    self._pricer.update(self._bookmap.get_book(self._ref_product),
                        self._bookmap.get_book(self._trade_product))

    if not self._pricer.ready or not self.is_og_ready() or self._dry_run:
      return

    pos = self.order_gateway.get_position(self._trade_product) + self._pos_bias
    sell_edge = self._edge
    if pos >= self._max_pos:
      sell_edge = self._close_edge
    elif pos >= 0:
      p = pos / self._max_pos
      assert 0. <= p <= 1.
      sell_edge = (1. - p) * self._edge + p * self._close_edge

    buy_edge = self._edge
    if pos <= self._min_pos:
      buy_edge = self._close_edge
    elif pos <= 0:
      p = pos / self._min_pos
      assert 0. <= p <= 1.
      buy_edge = (1. - p) * self._edge + p * self._close_edge

    askp, bidp = self._pricer.calculate_price(sell_edge=sell_edge, buy_edge=buy_edge, passive=True)
    self._executor.manage_mm_orders(askp, bidp)

    if book.timestamp - self._last_feature_dump_ts >= 2 * (10**9):
      self._last_feature_dump_ts = book.timestamp
      self._logger.info('Pos=%.3f, SellEdge=%.1fbp, BuyEdge=%.1fbp',
                        pos,
                        sell_edge * 10000.,
                        buy_edge * 10000.)
    '''
      self._logger.info(self._pricer.last_value.to_log_string())
    '''


def get_param_set(target, init_og_immediately=False):
  # Pricing
  trade_product = BitflyerFuturesProduct.FromStr('BTC-JPY.PERPETUAL')
  trade_product_true_book_funcs = ((lambda book: book.get_notional_asks_by_amt(1.)[1][-1][0]),
                                   (lambda book: book.get_notional_bids_by_amt(1.)[1][-1][0]))

  if target == '1':
    # ref_product = BitmexFuturesProduct.FromStr('BTC-USD.PERPETUAL')
    # ref_product_true_book_funcs = (
    #    (lambda book: book.get_notional_asks_by_qty(10000.)[1][-1][0]),
    #    (lambda book: book.get_notional_bids_by_qty(10000.)[1][-1][0]))

    ref_product = OkexFuturesProduct.FromStr('BTC-USD.QUARTER')
    ref_product_true_book_funcs = ((lambda book: book.get_notional_asks_by_qty(100.)[1][-1][0]),
                                   (lambda book: book.get_notional_bids_by_qty(100.)[1][-1][0]))

    basis_ma_window = 2
    edge = 7.
    close_edge = 5
    lot_size = 1.25  # BTC
    stack = 10
    pos_bias = 12.5
    og_config = 'bitflyer_japan2'
    strategy_name = '1.pass-unhedge.bitflyer-fxbtcjpy.japan2'

  elif target == '2':
    ref_product = BitmexFuturesProduct.FromStr('BTC-USD.PERPETUAL')
    ref_product_true_book_funcs = ((lambda book: book.get_notional_asks_by_qty(10000.)[1][-1][0]),
                                   (lambda book: book.get_notional_bids_by_qty(10000.)[1][-1][0]))

    # ref_product = OkexFuturesProduct.FromStr('BTC-USD.QUARTER')
    # ref_product_true_book_funcs = (
    #    (lambda book: book.get_notional_asks_by_qty(100.)[1][-1][0]),
    #    (lambda book: book.get_notional_bids_by_qty(100.)[1][-1][0]))

    basis_ma_window = 60
    edge = 8.
    close_edge = 2
    lot_size = 1.25  # BTC
    stack = 10
    pos_bias = 12.5
    og_config = 'bitflyer_japan1'
    strategy_name = '2.pass-unhedge.bitflyer-fxbtcjpy.japan1'

  else:
    raise ValueError('Invalid target: %s' % target)

  NS_PER_MINUTE = 60 * (10**9)
  strategy_params = {
      'currencies': [BitflyerCurrency.FromStrCurrency('JPY')],
      'trade_product': trade_product,
      'ref_product': ref_product,
      'edge_bp': edge,
      'close_edge_bp': close_edge,
      'pos_bias': pos_bias,
      'pricing': {
          'price_multiplier_1': 107.3,
          'basis_ma_window': basis_ma_window * NS_PER_MINUTE,
          'tick': 1.,
          'book_askt_func_1': ref_product_true_book_funcs[0],
          'book_bidt_func_1': ref_product_true_book_funcs[1],
          'book_askt_func_2': trade_product_true_book_funcs[0],
          'book_bidt_func_2': trade_product_true_book_funcs[1]
      },
      'executor': {
          'product': trade_product,
          'pos_bias': pos_bias,
          'lot_size': lot_size,
          'stickiness': 0.,
          'min_pos': -lot_size * stack,
          'max_pos': lot_size * stack,
          'order_update_period': 8.,
      },
      'logger': {
          'strategy_name': strategy_name,
          'strategy_group': 'pass_unhedge.bitflyer-fxbtcjpy',
          'trader': 'jaewon'
      }
  }

  strategy_params['logger']['strategy_name'] = strategy_params['logger']['strategy_name'].replace(
      '.', '_')
  strategy_params['logger']['strategy_group'] = strategy_params['logger']['strategy_group'].replace(
      '.', '_')

  # Trading environment config
  if init_og_immediately:
    init_og_after_min = 0
  else:
    init_og_after_min = basis_ma_window + 0.1

  exit_after_min = init_og_after_min + 30
  env_config = {
      'og_config_name': og_config,
      'init_order_gateway_after': datetime.timedelta(minutes=init_og_after_min),
      'exit_after': datetime.timedelta(minutes=exit_after_min)
  }

  return strategy_params, env_config


def main(argv):
  strategy_params, env_config = get_param_set(argv[1], init_og_immediately=False)

  # Run strategy!
  strategy = PassiveUnhedgeStrategy(strategy_params, dry_run=False)
  return run_live(strategy, **env_config)


if __name__ == '__main__':
  logging.basicConfig(
      level='DEBUG',
      format='%(levelname)8s %(asctime)s %(name)s %(filename)s:%(lineno)d] %(message)s')
  logging.getLogger('requests').setLevel(logging.WARNING)
  logging.getLogger('urllib3').setLevel(logging.WARNING)
  logging.getLogger('coin.exchange.base.rest.private_queryer_base').setLevel(logging.WARNING)
  logging.getLogger('coin.exchange.okex_futures.ws.subscriber').setLevel(logging.WARNING)
  sys.exit(main(sys.argv))
